﻿using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using WeavingCommon;

namespace NotifyPropertyWeaverMsBuildTask
{

    public class PropertyWeaver
    {
        MsCoreReferenceFinder msCoreReferenceFinder;
        Logger logger;
        PropertyData propertyData;
        TypeNode typeNode;
        TypeEqualityFinder typeEqualityFinder;
    	MethodBody setMethodBody;
        Collection<Instruction> instructions;
        Instruction lastInstruction;

        public PropertyWeaver(MsCoreReferenceFinder msCoreReferenceFinder, Logger logger, PropertyData propertyData, TypeNode typeNode, TypeEqualityFinder typeEqualityFinder)
        {
            this.msCoreReferenceFinder = msCoreReferenceFinder;
            this.logger = logger;
            this.propertyData = propertyData;
            this.typeNode = typeNode;
            this.typeEqualityFinder = typeEqualityFinder;
        }
 
        public void Execute()
        {

            logger.LogMessage("\t\t" + propertyData.PropertyDefinition.Name);
            var property = propertyData.PropertyDefinition;
            setMethodBody = property.SetMethod.Body;
            instructions = property.SetMethod.Body.Instructions;

            lastInstruction = setMethodBody.Instructions.Last();

            logger.LogMessage("\t\t\tCheckForEquality=" + propertyData.CheckForEquality);
            if (propertyData.CheckForEquality)
            {
                AddEqualityCheck();
            }

            AddEventInvokeCall(propertyData.PropertyDefinition);

            var propertyDefinitions = propertyData.AlsoNotifyFor.Distinct();

            foreach (var propertyDefinition in propertyDefinitions)
            {
                AddEventInvokeCall(propertyDefinition);
            }
            setMethodBody.InitLocals = true;

        }

        void AddEqualityCheck()
        {
            var variableDefinition = new VariableDefinition(msCoreReferenceFinder.BoolTypeReference);
            setMethodBody.Variables.Add(variableDefinition);
            if (propertyData.BackingFieldReference == null)
            {
                CheckAgainstProperty();
            }
            else
            {
                CheckAgainstField();
            }
        }

        void CheckAgainstField()
        {
            var fieldReference = propertyData.BackingFieldReference.GetGeneric();
            var typeEqualityMethod = typeEqualityFinder.Find(fieldReference.FieldType);
            if (typeEqualityMethod == null)
            {
                instructions.Prepend(Instruction.Create(OpCodes.Ldarg_0),
                                     Instruction.Create(OpCodes.Ldfld, fieldReference),
                                     Instruction.Create(OpCodes.Box, fieldReference.FieldType),
                                     Instruction.Create(OpCodes.Ldarg_1),
                                     Instruction.Create(OpCodes.Box, fieldReference.FieldType),
                                     Instruction.Create(OpCodes.Call, msCoreReferenceFinder.EqualsMethodReference),
                                     Instruction.Create(OpCodes.Stloc_0),
                                     Instruction.Create(OpCodes.Ldloc_0),
                                     Instruction.Create(OpCodes.Brtrue, lastInstruction));
            }
            else
            {
                instructions.Prepend(Instruction.Create(OpCodes.Ldarg_0),
                                     Instruction.Create(OpCodes.Ldfld, fieldReference),
                                     Instruction.Create(OpCodes.Ldarg_1),
                                     Instruction.Create(OpCodes.Call, typeEqualityMethod),
                                     Instruction.Create(OpCodes.Stloc_0),
                                     Instruction.Create(OpCodes.Ldloc_0),
                                     Instruction.Create(OpCodes.Brtrue, lastInstruction));
            }
        }

        void CheckAgainstProperty()
        {
            var propertyReference = propertyData.PropertyDefinition;
            var typeEqualityMethod = typeEqualityFinder.Find(propertyReference.PropertyType);
            var methodDefinition = propertyData.PropertyDefinition.GetMethod.GetGeneric();
            if (typeEqualityMethod == null)
            {
                instructions.Prepend(Instruction.Create(OpCodes.Ldarg_0),
                                     Instruction.Create(OpCodes.Call, methodDefinition),
                                     Instruction.Create(OpCodes.Box, propertyReference.PropertyType),
                                     Instruction.Create(OpCodes.Ldarg_1),
                                     Instruction.Create(OpCodes.Box, propertyReference.PropertyType),
                                     Instruction.Create(OpCodes.Call, msCoreReferenceFinder.EqualsMethodReference),
                                     Instruction.Create(OpCodes.Stloc_0),
                                     Instruction.Create(OpCodes.Ldloc_0),
                                     Instruction.Create(OpCodes.Brtrue, lastInstruction));
            }
            else
            {
                instructions.Prepend(Instruction.Create(OpCodes.Ldarg_0),
                                     Instruction.Create(OpCodes.Call, methodDefinition),
                                     Instruction.Create(OpCodes.Ldarg_1),
                                     Instruction.Create(OpCodes.Call, typeEqualityMethod),
                                     Instruction.Create(OpCodes.Stloc_0),
                                     Instruction.Create(OpCodes.Ldloc_0),
                                     Instruction.Create(OpCodes.Brtrue, lastInstruction));
            }
        }

        void AddEventInvokeCall(PropertyDefinition property)
        {
            logger.LogMessage(string.Format("\t\t\t{0}", property.Name));
            if (typeNode.EventInvoker.IsBeforeAfter)
            {
                AddBeforeAfrerInvokerCall(property);
            }
            else
            {
                AddSimpleInvokerCall(property);
            }
        }

        void AddSimpleInvokerCall(PropertyDefinition property)
        {
        	var toBeInserted = new[]
        	                   	{
        	                   		Instruction.Create(OpCodes.Ldarg_0),
        	                   		Instruction.Create(OpCodes.Ldstr, property.Name),
        	                   		CreateCall(typeNode.EventInvoker)
        	                   	};
			instructions.BeforeLast(toBeInserted);
			FixInstructionsPointingToTheLastOne(toBeInserted.First());
        }

        public Instruction CreateCall(EventInvokerMethod methodReference)
        {
            if (methodReference.IsVirtual)
            {
                return Instruction.Create(OpCodes.Callvirt, typeNode.EventInvoker.MethodReference);
            }
            return Instruction.Create(OpCodes.Call, typeNode.EventInvoker.MethodReference);
        }
        void AddBeforeAfrerInvokerCall(PropertyDefinition property)
        {
            var beforeVariable = new VariableDefinition(msCoreReferenceFinder.ObjectTypeReference);
            setMethodBody.Variables.Add(beforeVariable);
            var afterVariable = new VariableDefinition(msCoreReferenceFinder.ObjectTypeReference);
            setMethodBody.Variables.Add(afterVariable);
            var isVirtual = property.GetMethod.IsVirtual;
            var getMethod = property.GetMethod.GetGeneric();
            instructions.Prepend(Instruction.Create(OpCodes.Ldarg_0),
                                 CreateCall(getMethod, isVirtual),
                //TODO: look into why this box is required
                                 Instruction.Create(OpCodes.Box, property.GetMethod.ReturnType),
                                 Instruction.Create(OpCodes.Stloc, beforeVariable));

            var toBeInserted = new[]
                                   {
                                       Instruction.Create(OpCodes.Ldarg_0),
                                       CreateCall(getMethod, isVirtual),
                                       //TODO: look into why this box is required
                                       Instruction.Create(OpCodes.Box, property.GetMethod.ReturnType),
                                       Instruction.Create(OpCodes.Stloc, afterVariable),
                                       Instruction.Create(OpCodes.Ldarg_0),
                                       Instruction.Create(OpCodes.Ldstr, property.Name),
                                       Instruction.Create(OpCodes.Ldloc, beforeVariable),
                                       Instruction.Create(OpCodes.Ldloc, afterVariable),
                                       CreateCall(typeNode.EventInvoker),
                                   };

            instructions.BeforeLast(toBeInserted);
            FixInstructionsPointingToTheLastOne(toBeInserted.First());
        }
        public Instruction CreateCall(MethodReference methodReference, bool isVirtual)
        {
            if (isVirtual)
            {
                return Instruction.Create(OpCodes.Callvirt, methodReference);
            }
            return Instruction.Create(OpCodes.Call, methodReference);
        }

        void FixInstructionsPointingToTheLastOne(Instruction firstInstructionInserted)
    	{
    		//Fix everything pointing to the last instruction and ignore the first
    		var last = instructions[instructions.Count - 1];
			
    		//If CheckForEquality is true then skip the GotoLast of the equality injected.
    		var instructionsToFix = propertyData.CheckForEquality ? instructions.SkipWhile(i => i.Operand != last).Skip(1) : instructions;
    		foreach (var instruction in instructionsToFix.Where(instruction => instruction.Operand == last))
    		{
    			instruction.Operand = firstInstructionInserted;
    		}
    	}
    }
}